|
mruby 4.0.0
mruby is the lightweight implementation of the Ruby language
|
This document describes mruby's virtual machine for developers working on src/vm.c and related code.
Read this if you are: debugging method dispatch or call frame issues, working on exception handling, implementing new opcodes, modifying fiber/coroutine behavior, or optimizing the dispatch loop.
For the instruction set, see opcode.md. For the compiler that generates bytecode, see compiler.md.
mruby uses a register-based VM. Local variables and temporaries occupy fixed register slots determined at compile time. Each method call gets its own register window on a shared value stack.
The VM state is stored in mrb_context:
The value stack and call info stack grow independently. Each fiber has its own mrb_context.
STACK_INIT_SIZE)CALLINFO_INIT_SIZE)MRB_STACK_EXTEND_DOUBLING)MRB_STACK_GROWTH)MRB_STACK_MAX (0x40000 - 128)MRB_CALL_LEVEL_MAX (512, or 128 with ASAN)Exceeding either limit raises SystemStackError.
When the value stack is reallocated, all REnv objects and mrb_callinfo stack pointers are adjusted by the delta (envadjust function).
Each method or block call pushes a mrb_callinfo frame:
The n and nk fields are 4 bits each (0-15). When n == 15, positional arguments are packed into a single Array in register 1. When nk == 15, keyword arguments are packed into a single Hash.
The block index is calculated by mrb_bidx(n, nk):
| Value | Name | Meaning |
|---|---|---|
| 0 | CINFO_NONE | Normal VM-to-VM call |
| 1 | CINFO_DIRECT | Explicit VM call (block, lambda.call) |
| 2 | CINFO_SKIP | Skip frame in stack traces |
| 3 | CINFO_RESUMED | Fiber resumed (stop execution) |
The main loop in mrb_vm_run() decodes and dispatches opcodes. Two dispatch strategies are available:
optable[]) for direct dispatch. Faster due to better branch prediction.MRB_USE_VM_SWITCH_DISPATCH): a standard switch(insn) statement. Default on MSVC and other compilers.The dispatch loop is wrapped in MRB_TRY/MRB_CATCH for exception handling (see Exception Handling).
When OP_SEND (or OP_SSEND, OP_SUPER) executes:
Determine argument layout. If argument count < 15, the fast path uses inline registers. Otherwise, arguments are packed into an Array (varargs mode).
The new frame's stack starts at the previous frame's stack + a (the receiver's register index).
The lookup sequence:
(class, mid). Default cache size: MRB_METHOD_CACHE_SIZE (256 entries).mt), then walk the superclass chain.The method cache is invalidated when classes are modified (mrb_mc_clear_by_class).
irep->nregs, set ci->pc to irep->iseq, and jump to the new bytecode.func(mrb, recv) directly, then pop the call frame and store the return value.Private methods are only callable without an explicit receiver. Protected methods are callable from the same class hierarchy. Violations raise NoMethodError.
By default, mruby uses setjmp/longjmp for exception control flow:
With MRB_USE_CXX_EXCEPTION, C++ try/catch is used instead.
Each irep contains a catch handler table (appended after iseq in memory) with entries for rescue and ensure blocks:
When an exception occurs:
ensure handler is found: execute it (may re-raise)rescue handler is found: jump to handler codecipop) and repeat with the parent frameCINFO_DIRECT frames are destroyed during propagationClosures capture their enclosing scope's variables through REnv:
While the defining scope is active, REnv::stack points directly into the VM value stack (shared). This avoids copying.
When a closure outlives its defining scope, mrb_env_unshare() copies the captured variables from the stack to a heap-allocated buffer:
After unsharing, MRB_ENV_CLOSE(env) sets cxt = NULL to indicate the environment is detached. A write barrier is issued for GC correctness.
| Flag | Meaning |
|---|---|
MRB_PROC_CFUNC_FL | C function (not irep-based) |
MRB_PROC_STRICT | Lambda (strict argument check) |
MRB_PROC_ORPHAN | No environment attachment |
MRB_PROC_ENVSET | Has captured environment |
MRB_PROC_SCOPE | Defines a new variable scope |
Fibers are lightweight coroutines. Each fiber has its own mrb_context with separate value and call info stacks.
On Fiber#resume:
mrb->c to the fiber's contextOn Fiber.yield:
mrb->c = c->prev)When a fiber completes (fiber_terminate):
TERMINATEDFibers cannot yield across C function boundaries. You cannot call Fiber.yield from within a C-implemented method (except via mrb_fiber_yield at return). This is because C call frames cannot be suspended and resumed.
The VM saves the arena index at the start of the dispatch loop:
After each C function call, the arena is shrunk back:
This prevents temporary objects created by C functions from accumulating in the arena.
Write barriers are issued when environments are detached or closed, ensuring the incremental GC correctly tracks live references.
| File | Contents |
|---|---|
src/vm.c | Dispatch loop, method invocation (~1900 lines) |
include/mruby.h | mrb_state, mrb_callinfo, mrb_context |
include/mruby/proc.h | RProc, REnv structures |
include/mruby/throw.h | MRB_TRY/MRB_CATCH macros |